/*
 * $Id: TableSearchable.java 3194 2009-01-21 11:39:19Z kleopatra $
 *
 * Copyright 2007 Sun Microsystems, Inc., 4150 Network Circle,
 * Santa Clara, California 95054, U.S.A. All rights reserved.
 *
 * This library is free software; you can redistribute it and/or
 * modify it under the terms of the GNU Lesser General Public
 * License as published by the Free Software Foundation; either
 * version 2.1 of the License, or (at your option) any later version.
 * 
 * This library is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
 * Lesser General Public License for more details.
 * 
 * You should have received a copy of the GNU Lesser General Public
 * License along with this library; if not, write to the Free Software
 * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301  USA
 */
package org.jdesktop.swingx.search;

import java.awt.Rectangle;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import org.jdesktop.swingx.JXTable;
import org.jdesktop.swingx.decorator.AbstractHighlighter;
import org.jdesktop.swingx.decorator.Highlighter;

/**
 * An Searchable implementation for use in JXTable.
 * 
 * @author Jeanette Winzenburg
 */
public class TableSearchable extends AbstractSearchable {

	/** The target JXTable. */
	protected JXTable table;

	/**
	 * Instantiates a TableSearchable with the given table as target.
	 * 
	 * @param table
	 *            the JXTable to search.
	 */
	public TableSearchable(JXTable table) {
		this.table = table;
	}

	/**
	 * {@inheritDoc}
	 * <p>
	 * 
	 * This implementation loops through the cells in a row to find a match.
	 */
	@Override
	protected void findMatchAndUpdateState(Pattern pattern, int startRow, boolean backwards) {
		SearchResult matchRow = null;
		if (backwards) {
			// CHECK: off-one end still needed?
			// Probably not - the findXX don't have side-effects any longer
			// hmmm... still needed: even without side-effects we need to
			// guarantee calling the notfound update at the very end of the
			// loop.
			for (int r = startRow; r >= -1 && matchRow == null; r--) {
				matchRow = findMatchBackwardsInRow(pattern, r);
				updateState(matchRow);
			}
		} else {
			for (int r = startRow; r <= getSize() && matchRow == null; r++) {
				matchRow = findMatchForwardInRow(pattern, r);
				updateState(matchRow);
			}
		}
		// KEEP - JW: Needed to update if loop wasn't entered!
		// the alternative is to go one off in the loop. Hmm - which is
		// preferable?
		// updateState(matchRow);

	}

	/**
	 * {@inheritDoc}
	 * <p>
	 * 
	 * Implemented to search for an extension in the cell given by row and
	 * foundColumn.
	 */
	@Override
	protected SearchResult findExtendedMatch(Pattern pattern, int row) {
		return findMatchAt(pattern, row, lastSearchResult.foundColumn);
	}

	/**
	 * Searches forward through columns of the given row. Starts at
	 * lastFoundColumn or first column if lastFoundColumn < 0. returns an
	 * appropriate SearchResult if a matching cell is found in this row or null
	 * if no match is found. A row index out off range results in a no-match.
	 * 
	 * @param pattern
	 *            <code>Pattern</code> that we will try to locate
	 * @param row
	 *            the row to search
	 * @return an appropriate <code>SearchResult</code> if a matching cell is
	 *         found in this row or null if no match is found
	 */
	private SearchResult findMatchForwardInRow(Pattern pattern, int row) {
		int startColumn = (lastSearchResult.foundColumn < 0) ? 0 : lastSearchResult.foundColumn;
		if (isValidIndex(row)) {
			for (int column = startColumn; column < table.getColumnCount(); column++) {
				SearchResult result = findMatchAt(pattern, row, column);
				if (result != null)
					return result;
			}
		}
		return null;
	}

	/**
	 * Searches forward through columns of the given row. Starts at
	 * lastFoundColumn or first column if lastFoundColumn < 0. returns an
	 * appropriate SearchResult if a matching cell is found in this row or null
	 * if no match is found. A row index out off range results in a no-match.
	 * 
	 * @param pattern
	 *            <code>Pattern</code> that we will try to locate
	 * @param row
	 *            the row to search
	 * @return an appropriate <code>SearchResult</code> if a matching cell is
	 *         found in this row or null if no match is found
	 */
	private SearchResult findMatchBackwardsInRow(Pattern pattern, int row) {
		int startColumn = (lastSearchResult.foundColumn < 0) ? table.getColumnCount() - 1 : lastSearchResult.foundColumn;
		if (isValidIndex(row)) {
			for (int column = startColumn; column >= 0; column--) {
				SearchResult result = findMatchAt(pattern, row, column);
				if (result != null)
					return result;
			}
		}
		return null;
	}

	/**
	 * Matches the cell content at row/col against the given Pattern. Returns an
	 * appropriate SearchResult if matching or null if no matching
	 * 
	 * @param pattern
	 *            <code>Pattern</code> that we will try to locate
	 * @param row
	 *            a valid row index in view coordinates
	 * @param column
	 *            a valid column index in view coordinates
	 * @return an appropriate <code>SearchResult</code> if matching or null
	 */
	protected SearchResult findMatchAt(Pattern pattern, int row, int column) {
		String text = table.getStringAt(row, column);
		if ((text != null) && (text.length() > 0)) {
			Matcher matcher = pattern.matcher(text);
			if (matcher.find()) {
				return createSearchResult(matcher, row, column);
			}
		}
		return null;
	}

	/**
	 * 
	 * {@inheritDoc}
	 * <p>
	 * 
	 * Overridden to adjust the column index to -1.
	 */
	@Override
	protected int adjustStartPosition(int startIndex, boolean backwards) {
		lastSearchResult.foundColumn = -1;
		return super.adjustStartPosition(startIndex, backwards);
	}

	/**
	 * {@inheritDoc}
	 * <p>
	 * 
	 * Overridden to loop through all columns in a row.
	 */
	@Override
	protected int moveStartPosition(int startRow, boolean backwards) {
		if (backwards) {
			lastSearchResult.foundColumn--;
			if (lastSearchResult.foundColumn < 0) {
				startRow--;
			}
		} else {
			lastSearchResult.foundColumn++;
			if (lastSearchResult.foundColumn >= table.getColumnCount()) {
				lastSearchResult.foundColumn = -1;
				startRow++;
			}
		}
		return startRow;
	}

	/**
	 * {@inheritDoc}
	 * <p>
	 * 
	 * Overridden to check the column index of last find.
	 */
	@Override
	protected boolean isEqualStartIndex(final int startIndex) {
		return super.isEqualStartIndex(startIndex) && isValidColumn(lastSearchResult.foundColumn);
	}

	/**
	 * Checks if row is in range: 0 <= row < getRowCount().
	 * 
	 * @param column
	 *            the column index to check in view coordinates.
	 * @return true if the column is in range, false otherwise
	 */
	private boolean isValidColumn(int column) {
		return column >= 0 && column < table.getColumnCount();
	}

	/**
	 * {@inheritDoc}
	 */
	@Override
	protected int getSize() {
		return table.getRowCount();
	}

	/**
	 * {@inheritDoc}
	 */
	@Override
	public JXTable getTarget() {
		return table;
	}

	/**
	 * Configures the match highlighter to the current match. Ensures that the
	 * matched cell is visible, if there is a match.
	 * 
	 * PRE: markByHighlighter
	 * 
	 */
	protected void moveMatchByHighlighter() {
		AbstractHighlighter searchHL = getConfiguredMatchHighlighter();
		// no match
		if (!hasMatch()) {
			return;
		} else {
			ensureInsertedSearchHighlighters(searchHL);
			table.scrollCellToVisible(lastSearchResult.foundRow, lastSearchResult.foundColumn);
		}
	}

	/**
	 * {@inheritDoc}
	 * <p>
	 * 
	 * Overridden to convert the column index in the table's view coordinate
	 * system to model coordinate.
	 * <p>
	 * 
	 * PENDING JW: this is only necessary because the SearchPredicate wants its
	 * highlight column in model coordinates. But code comments in the
	 * SearchPredicate seem to indicate that we probably want to revise that
	 * (legacy?).
	 */
	@Override
	protected int convertColumnIndexToModel(int viewColumn) {
		return getTarget().convertColumnIndexToModel(viewColumn);
	}

	/**
	 * Moves the row selection to the matching cell and ensures its visibility,
	 * if any. Does nothing if there is no match.
	 * 
	 */
	protected void moveMatchBySelection() {
		if (!hasMatch()) {
			return;
		}
		int row = lastSearchResult.foundRow;
		int column = lastSearchResult.foundColumn;
		table.changeSelection(row, column, false, false);
		if (!table.getAutoscrolls()) {
			// scrolling not handled by moving selection
			Rectangle cellRect = table.getCellRect(row, column, true);
			if (cellRect != null) {
				table.scrollRectToVisible(cellRect);
			}
		}
	}

	/**
	 * {@inheritDoc}
	 * <p>
	 */
	@Override
	protected void moveMatchMarker() {
		if (markByHighlighter()) {
			moveMatchByHighlighter();
		} else { // use selection
			moveMatchBySelection();
		}
	}

	/**
	 * {@inheritDoc}
	 * <p>
	 */
	@Override
	protected void removeHighlighter(Highlighter searchHighlighter) {
		table.removeHighlighter(searchHighlighter);
	}

	/**
	 * {@inheritDoc}
	 * <p>
	 */
	@Override
	protected Highlighter[] getHighlighters() {
		return table.getHighlighters();
	}

	/**
	 * {@inheritDoc}
	 * <p>
	 */
	@Override
	protected void addHighlighter(Highlighter highlighter) {
		table.addHighlighter(highlighter);
	}

}